# 👉 npm 包管理

# npm 进化史

# npm2 的嵌套地狱

最早期的 npm 版本(npm v2),npm 在安装依赖的时候会将依赖放到 node_modules 文件中;同时,如果某个直接依赖 A ,A 又依赖于其他的依赖包 B,那么依赖 B 会作为间接依赖,安装到依赖 A 的文件夹 node_modules 中,然后可能多个包之间也会有出现同样的依赖递归的。

依赖嵌套地狱

如果项目一旦过大,那么必然会形成一棵巨大的依赖树,依赖包会出现重复,从而形成嵌套地狱。

# npm3 的扁平依赖

为了解决 npm2 的嵌套地狱,npm3 进行了依赖扁平化优化,将共同的间接依赖也变成直接依赖,

npm2和npm3

但是,npm 3.x 版本并未完全解决老版本的模块冗余问题。如果上面例子中的依赖 A 和依赖 C 分别依赖 B 的不同版本,npm v3 并不会将两个不同的 B 版本都提出。而是会通过 localeCompare 方法对依赖进行一次排序,最终字典序在前面的 npm 包的底层依赖会被优先提出来,其他版本的 B 依然会嵌套在原来依赖 B 的包下。上面的例子来说,就是依赖版本 Bv2.0 依然嵌套在 C 下。

npm3依赖

# npm install 过程做了啥

在我们敲下 npm install 的时候 , npm 会做以下几件事情:

  • 检查 config
    npm 执行会先读取 npm config list 和 npmrc 配置,而 npmrc 是有优先级之分的,后面会展开讲讲。
  • 检查是否存在 package-lock.json
    • 存在 package-lock.json,将会检查跟当前 package-lock 里声明的版本是否一致
      • 版本一致,是否有检查缓存
      • 版本不一致 , 对应处理方法跟 npm 版本有关。在最新版本的 npm 中,会检查依赖包兼容版本, 如果版本能兼容,则按照 package-lock 版本安装,反之按照 package.json 版本安装
    • 不存在 package-lock.json,将会执行以下步骤:
      • 获取依赖包的信息
      • 构建依赖树
      • 扁平化
      • 检查缓存
  • 检查缓存
    • 如果有缓存,将对应缓存解压到 node_modules 下,生成 package-lock
    • 如果没有缓存:
      • 下载资源包,检查资源包完整性
      • 添加到缓存中
      • 解压到 node_modules,生成 package-lock

由上面的步骤可以看出,缓存功能在 npm 整个过程中起到非常关键的作用 ,每次安装依赖时都会对对应包创建缓存,那我们如何查看缓存呢?

# npm 缓存

我们可以通过npm config get cache这个命令获取 npm 缓存在本地的路径,进入该目录后,访问_cacache目录 , npm 的缓存就放在该目录下。

~      % npm config get cache
/Users/chieminchan/.npm
~      % cd /Users/chieminchan/.npm
~/.npm % ls
_cacache   _locks   _logs  _npx  anonymous-cli-metrics.json  index-v5

HBJWOe.png

其中 content-v2 是缓存二进制文件 , index-v5 是缓存对应索引 hash.

# npm 是如何利用缓存的

npm 在执行安装时,可以根据 package-lock.json 中存储的 integrityversionname 生成一个唯一的 key 对应到 index-v5 目录下的缓存记录,从而找到依赖包tar的 hash,然后根据 hash 再去找缓存直接使用。

# 举个例子 🌰

我们在缓存索引目录 index-v5  下搜索一个clean-webpack-plugin-3.0.0.tgz包测试一下。

  1. grep 命令找到依赖包在 index-v5 的位置

    .npm/_cacache % grep "https://registry.npm.taobao.org/clean-webpack-plugin/download/clean-webpack-plugin-3.0.0.tgz" -r ./index-v5/
    
    ./index-v5//d0/84/e30448aecdc817bb6a8706d91392......(省略)
    

    bpdxoT.png

  2. ./index-v5//d0/84/找到依赖包索引文件,格式化 json 得:

    {
        "key": "pacote:tag-manifest:https://registry.npm.taobao.org/clean-webpack-plugin/download/clean-webpack-plugin-3.0.0.tgz:sha1-qZ2Ow0wcYopFQVZ6p7RXRGRgxis=",
        "integrity": "sha512-C2EkHXwXvLsbrucJTRS3xFHv7Mf/y9klmKDxPTE8yevCoH5h8Ae69Y+/lP+ahpW91crnzgO78elOk2E6APJfIQ==",
        "time": 1598713645175,
        "size": 1,
        "metadata": {
            "id": "clean-webpack-plugin@3.0.0",
            "manifest": {
                "name": "clean-webpack-plugin",
                "version": "3.0.0",
                "engines": {
                    "node": ">=8.9.0"
                },
                "dependencies": {
                    //...
                },
                "devDependencies": {
                    //...
                },
                "bundleDependencies": false,
                "peerDependencies": {
                    "webpack": "*"
                },
                "deprecated": false,
                "_resolved": "https://registry.npm.taobao.org/clean-webpack-plugin/download/clean-webpack-plugin-3.0.0.tgz",
                "_integrity": "sha1-qZ2Ow0wcYopFQVZ6p7RXRGRgxis=",
                "_shasum": "a99d8ec34c1c628a4541567aa7b457446460c62b",
                "_shrinkwrap": null,
                "bin": null,
                "_id": "clean-webpack-plugin@3.0.0"
            },
            "type": "finalized-manifest"
        }
    }
    

    其中 _shasum 属性 a99d8ec34c1c628a4541567aa7b457446460c62b 即为 tar 依赖包的  hashhash的前几位 a99d8  即为缓存的前两层目录。

  3. 根据 index-v5 索引文件中得到的目录路径,就可以在content-v2中找到对应的压缩后的依赖包: bp01CF.png

以上的缓存策略是从 npm v5 版本开始的,在 npm v5 版本之前,每个缓存的模块在 ~/.npm 文件夹中以模块名的形式直接存储,储存结构是{cache}/{name}/{version}。

# 如何生成缓存

npm 本身只提供清除缓存和验证缓存完整性的方法,不提供直接操作缓存的方法,可以通过 npm cache 来操作这些缓存数据。

npm 是如何对依赖包进行本地化缓存的,即_content_v2index-v5的缓存资源又是如何生成的?

上官方文档链接:https://docs.npmjs.com/cli/v8/commands/npm-cache#details

大概意思如下:npm 会将所有的缓存数据存储在已配置的 cache 路径下,命名为_cacache 的目录中。_cache存这个目录主要是通过 pacote去获取到缓存资源,它存储所有的 http 请求数据以及其他包相关的数据。缓存的资源在插入和提取时都会经过完整验证,如果有损坏将会触发错误或者触发pacote,自动进行重新获取数据的信号。因此,除非是需要回收磁盘空间,缓存文件没必要进行清除,npm 也不会自行删除数据,当我们npm clean cache时需要--force运行。

也由此可知,npm 主要是用 pacote (opens new window) 来处理依赖包的。那什么是 pacote ?pacote 是 npm 用来下载依赖包元数据的模块,大概的思路是结合网络请求和文件读写配置进行依赖包资源的写入和生成对应的压缩文件。

npm 主要有以下三个地方会用到 pacote:

  • npm install xx 执行的时候,如果存在缓存时,将会通过 pacote.extract 把缓存包解压到对应的 node_modules 下面

  • npm install xx 执行时,不存在缓存,将会先进行资源下载和完整性校验,接着运行npm cache add xx,通过pacote.tarball.stream.npm/_cacache 里增加缓存数据

  • npm pack xxx 会通过 pacote.tarball.toFile 在当前路径生成对应的压缩文件

而 pacote 又是依赖 npm-registry-fetch 来下载包,在给指定路径下根据 IETF RFC 7234 (opens new window) 生成缓存数据。

# npmrc 是啥

.npmrc,可以理解成npm running cnfiguration, 即 npm 运行时配置文件。我们知道,npm 最大的作用就是帮助开发者安装需要的依赖包,但是要从哪里下载?下载哪一个版本的包,把包下载到电脑的哪个路径下?这些都可以在.npmrc 中进行配置。

# npmrc 是有权重的

npm 按照如下顺序读取这些配置文件:

  • 项目级的.npmrc 文件
    可以在项目的根目录下创建一个.npmrc 文件,只用于管理这个项目的 npm 安装

  • 用户级的 .npmrc 文件
    在你使用一个账号登陆的电脑的时候,可以为当前用户创建一个.npmrc 文件,之后用该用户登录电脑,就可以使用该配置文件。可以通过 npm config get userconfig 来获取该文件的位置

  • 全局级的 .npmrc
    一台电脑可能有多个用户,在这些用户之上,你可以设置一个公共的.npmrc文件,供所有用户使用。该文件的路径为:$PREFIX/etc/npmrc,使用 npm config get prefix 获取 \$PREFIX。如果你不曾配置过全局文件,该文件不存在。

  • npm 内置的 .npmrc
    基本上用不到,不用过度关注。

# 参考文章